/**
 * 
 */
package Verifier;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

import Verifier.ArithmeticSystem.ArithmeticConvertor;
import Verifier.ArithmeticSystem.Field;
import Verifier.ArithmeticSystem.FieldElement;
import Verifier.ArithmeticSystem.Group;
import Verifier.ArithmeticSystem.GroupElement;
import Verifier.ArithmeticSystem.ProductElement;
import Verifier.ArithmeticSystem.ProductGroup;
import Verifier.ByteTree.ByteTree;
import Verifier.ByteTree.ByteTreeElement;
import Verifier.ByteTree.Leaf;
import Verifier.ByteTree.Node;
import Verifier.Cryptography.HashFunction;
import Verifier.Cryptography.PRG;
import Verifier.FiatShamirProofsVerification.CorrectDecryptionFactorsProofVerifier;
import Verifier.FiatShamirProofsVerification.FiatShamirProofInfo;
import Verifier.FiatShamirProofsVerification.ShuffleOfCiphertextsConsistentCommitmentProofVerifier;
import Verifier.FiatShamirProofsVerification.ShuffleOfCommitmentsProofVerifier;
import Verifier.Marshal.MarshalParser;
import Verifier.xmlParser.ProtocolInfoParser;

/**
 * The main algorithm of the verifier. This class merges all the sub parts of the system into one working verifier.
 * 
 * Note: if changes are made to the groups which are used by the Verificatum, this is the class to fix.
 * 
 * @author Moshe Baavur
 *
 */
public class Verifier {
	String protocolFile;
	String proofDirectory;
	String publicKeyFile;
	String ciphertextsFile;
	String plaintextsFile;
	boolean verboseMode;
	byte[] prefix;
	ProductElement publicKey;
	GroupElement[] individualPublicKeys;
	ProductElement[] inputCipherTexts;
	ProductElement[] prevCipherTexts;
	ProductElement[] currentCipherTexts;
	GroupElement[] permutationCommitment; // denoted by u_l
	FiatShamirProofInfo info;
	ProductElement[][] allCipherTexts;
	int latestCipherTextIndex;
	GroupElement[][] allDecryptionFactors;
	
	// protocol variables
	int acceptedError; // noted as n_r
	int numChallengeBits; // noted as n_v
	int numVectorBits; // noted as n_e
	HashFunction hash; // noted as H
	int lambda;
	int maxCipherTexts; // noted as N_0
	int numCipherTexts; // noted as N
	int numParties; // noted as k
	Group worldGroup; // noted as G_q
	Group ciphertextsGroup; // noted as C
	Group plaintextsGroup; // noted as M
	Field randomizersField; // noted as R
	PRG prg; // noted as PRG(s)

	final public static int NumberOfPrimeTests = 100; // this is the number of tests that should run on the orders of the fields and groups, to check that they are prime
	
	/**
	 * Creates a new verifier object with the protocol file and the proof directory as its initial values
	 * 
	 * @param protocolFile the path to the protocol file
	 * @param proofDirectory the path to the proof directory
	 * @param verboseMode should we print information to the screen
	 */
	public Verifier(String protocolFile, String proofDirectory, boolean verboseMode) {
		this.protocolFile = protocolFile;
		this.proofDirectory = proofDirectory;
		this.publicKeyFile = null;
		this.ciphertextsFile = null;
		this.plaintextsFile = null;
		this.verboseMode = verboseMode;
		this.prefix = null;
		this.worldGroup = null;
		this.publicKey = null;
		this.individualPublicKeys = null;
		this.inputCipherTexts = null;
		this.currentCipherTexts = null;
		this.prevCipherTexts = null;
		this.allCipherTexts = null;
		this.latestCipherTextIndex = -1;
		this.allDecryptionFactors = null;
		this.ciphertextsGroup = null;
		this.plaintextsGroup = null;
		this.randomizersField = null;
		// create a new info object
		this.info = new FiatShamirProofInfo();
	}
	
	/**
	 * Creates a new verifier object with the protocol file and the proof directory and other values as its initial values
	 * 
	 * @param protocolFile the path to the protocol file
	 * @param proofDirectory the path to the proof directory
	 * @param publicKeyFile the path to the public key file - this file should be a text file
	 * @param ciphertextsFile the path to the cipher texts file - this file should be a text file
	 * @param plaintextsFile the path to the plain texts file - this file should be a text file
	 * @param verboseMode should we print information to the screen
	 */
	public Verifier(String protocolFile, String proofDirectory, String publicKeyFile, String ciphertextsFile, String plaintextsFile, boolean verboseMode) {
		this.protocolFile = protocolFile;
		this.proofDirectory = proofDirectory;
		this.publicKeyFile = publicKeyFile;
		this.ciphertextsFile = ciphertextsFile;
		this.plaintextsFile = plaintextsFile;
		this.verboseMode = verboseMode;
		this.prefix = null;
		this.worldGroup = null;
		this.publicKey = null;
		this.individualPublicKeys = null;
		this.inputCipherTexts = null;
		this.currentCipherTexts = null;
		this.prevCipherTexts = null;
		this.allCipherTexts = null;
		this.latestCipherTextIndex = -1;
		this.allDecryptionFactors = null;
		this.numCipherTexts = 0;
		this.ciphertextsGroup = null;
		this.plaintextsGroup = null;
		this.randomizersField = null;
		// create a new info object
		this.info = new FiatShamirProofInfo();
	}
	
	/**
	 * This function runs the entire test of the verifier, according to the algorithm in the verifier's document
	 * 
	 * @return true - if the algorithm succeeds
	 */
	public boolean RunTest() {
		logStringNewLine("", false); // print empty line
		logStringNewLine("Verificatum's Verifier : Running Test", true);
		logStringNewLine("===========================================================", false);
		
		// Step 2. read the parameters from the protocol file
		if (false == readPublicParametes()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		
		// Step 3. create the prefix from the protocol file
		if (false == createPrefix()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		
		// Step 4. read the joint public key
		if (false == readJointPublicKey()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		
		// Step 5. read individual public keys and check for consistency
		if (false == readIndividualPublicKeys()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		
		// Step 6. read the array of input ciphertexts
		if (false == readArrayOfInputCiphertexts()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		
		// Step 7. Proofs of shuffle
		if (false == proofsOfShuffle()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		
		// Step 8. Proofs of Decryption
		if (false == proofsOfDecryption()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		logStringNewLine("", false); // print empty line
		// Step 9. Verify the output
		if (false == verifyOutput()) {
			logStringNewLine("Verificatum's Verifier : Running Test : Failed", true);
			return (false);
		}
		// the verification was successful
		logStringNewLine("Verificatum's Verifier : Running Test : OK", true);
		return (true);
	}
	
	// this format is used for printing the dates
	private SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss dd/MM/yyyy");
	
	// prints a string to the screen if we are in verbose mode, without moving the cursor to a new line
	private void logString(String stringToLog, boolean printDate)
	{
		if (verboseMode) {
			// print the date
			if (printDate)
				System.out.print(dateFormat.format(new Date()) + " : ");
			System.out.print(stringToLog);
		}
	}
	
	// prints a string to the screen if we are in verbose mode, and move the cursor to a new line
	private void logStringNewLine(String stringToLog, boolean printDate)
	{
		if (verboseMode) {
			// print the date
			if (printDate)
				System.out.print(dateFormat.format(new Date()) + " : ");
			System.out.println(stringToLog);
		}
	}
	
	// prints a string to the screen if we are in verbose mode, and move the cursor to a new line
	private void logError(String errorString)
	{
		if (verboseMode) {
			// we don't log the date for errors
			String message = "*** [Error] : " + errorString + " ***";
			// print a title of stars
			for (int i = 0; i < message.length(); i++) {
				System.out.print("*");	
			}
			System.out.println();
			System.out.println(message);
			// print a bottom of stars
			for (int i = 0; i < message.length(); i++) {
				System.out.print("*");	
			}
			System.out.println();
		}
	}
	
	// reads the public parameters from the protocol file
	private boolean readPublicParametes()
	{
		logString("Step 2. Reading Public Parameters : ", true);
		
		try {
			// open the protocol file
			File xmlFile = new File(this.protocolFile);
			// and parse it
			ProtocolInfoParser xmlParser = new ProtocolInfoParser(xmlFile);
			// now, load all the parameters and save them - if reading one of them fails, we should fail
			this.acceptedError = xmlParser.getAcceptableStatisticalErr();
			this.numChallengeBits = xmlParser.getChallengeBitNum();
			this.numVectorBits = xmlParser.getVectorBitNum();
			this.hash = xmlParser.getHash();
			this.lambda = xmlParser.getLambda();
			this.maxCipherTexts = xmlParser.getMaxCiphertexts();
			this.numParties = xmlParser.getNumOfParties();
			this.worldGroup = xmlParser.getPGroup();
			this.prg = xmlParser.getPRG();
			
			// now we should create the randomizer field, the cipher texts and the plain texts groups
			
			// the current version of the document states that the groups and field are:
			// R = Z_q
			this.randomizersField = new Field(this.worldGroup.getOrder());
			// M = G_q
			this.plaintextsGroup = this.worldGroup;
			// C = G_q x G_q
			this.ciphertextsGroup = new ProductGroup(new Group[] { this.worldGroup, this.worldGroup });
			
			// note: if a change to the groups would be made, than this section should change
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		
		logStringNewLine("OK", false);
		return (true);
	}
	
	// create the prefix which should be used to create seeds and challenges
	private boolean createPrefix()
	{
		// open the protocol file again, but this time, read it as a byte array and then create the prefix
		logString("Step 3. Creating the Prefix for Random Oracles : ", true);
		
		InputStream inStream = null;
		try {
			// open the protocol file
			File protFile = new File(this.protocolFile);
			inStream = new FileInputStream(protFile);
			// get the size of the file
			long fileSize = protFile.length();
			byte[] data = new byte[(int) fileSize];
			// read all the contents of the file
			inStream.read(data, 0, (int)fileSize);
			// and hash it
			byte[] fileData = this.hash.HashDigest(data);
			prefix = fileData;
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		} finally {
			if (null != inStream)
				try {
					inStream.close();
				} catch (IOException e) {
					logStringNewLine("Failed", false);
					logError(e.getMessage());
				}
		}
		
		logStringNewLine("OK", false);
		return (true);
	}
	
	// read the joint public key from the file and validate the data inside the file
	private boolean readJointPublicKey()
	{
		// try to read the joint public key
		logString("Step 4. Reading Joint Public Key : ", true);
		
		// read the key from the FullPublicKey.bt file
		ByteTree publicKeyTree = null;
		try {
			if (null == publicKeyFile)
				publicKeyTree = readBTFile(getFilepathInFolder("FullPublicKey.bt"));
			else
				publicKeyTree = readBTTextFile(publicKeyFile);
			
			// if failed to read the ByteTree
			if (null == publicKeyTree) {
				throw new Exception("Failed to read the public key file");
			}
			// divide the byte tree into 2 byte trees 
			ByteTreeElement rootElement = publicKeyTree.getFirstByteTreeElement();
			if (rootElement instanceof Leaf) {
				throw new Exception("Public key tree is malformed");
			}
			// check that the number of children is correct
			Node rootNode = (Node)rootElement;
			if (2 != rootNode.getDataLength()) {
				throw new Exception("Public key tree has incorrect number of children");
			}
			
			// now, convert the first child into a group
			Group keyGroup = MarshalParser.Unmarshal(new ByteTree(rootNode.getChild(0)));
			
			// check that the key group is the same as the world group
			if (!keyGroup.equals(worldGroup)) {
				throw new Exception("The public key is from unknown group");
			}
			
			// now, convert the second child into a product group element of the group - G_q x G_q
			ProductElement publicKey = ArithmeticConvertor.ToProductElement(new ByteTree(rootNode.getChild(1)), 
					new ProductGroup(new Group[] { this.worldGroup, this.worldGroup }));
			
			// this is the public key which should contain the first element as the creator of the group
			if (2 != publicKey.getNumberOfElements()) {
				throw new Exception("The product is not in the right size");
			}
			
			if (!publicKey.getElement(0).equals(worldGroup.getGroupCreatorAsGroupElement())) {
				throw new Exception("The first element in the group is not the generator");
			}
			
			// all is well, save the public key
			this.publicKey = publicKey;
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		
		logStringNewLine("OK", false);
		return (true);
	}
	
	// read all the partial public keys and check that their combination matches the joint public key
	private boolean readIndividualPublicKeys()
	{
		// try to read the individual public keys
		logString("Step 5. Reading Individual Public Keys : ", true);
		
		individualPublicKeys = new GroupElement[this.numParties];
		
		// read the keys from the PublicKey<l>.bt file
		try {
			for (int i = 0 ; i < individualPublicKeys.length;i++) {
				ByteTree publicKeyTree = null;
				publicKeyTree = readBTFile(getFilepathInFolder("PublicKey" + String.format("%02d", (i + 1))  + ".bt"));
				
				// if failed to read the ByteTree
				if (null == publicKeyTree) {
					throw new Exception("Failed to read the public key file");
				}
				// now, convert the byte tree into a product group element
				individualPublicKeys[i] = ArithmeticConvertor.ToGroupElement(publicKeyTree, worldGroup);
			}
			
			// all went well. now we should verify that we can go from the partial public keys to the main public key
			GroupElement multiplicationElement = worldGroup.getGroupIdentityElement();
			for (int i = 0 ; i < individualPublicKeys.length;i++) {
				multiplicationElement = multiplicationElement.multiply(individualPublicKeys[i]);
			}
			// check that y_0*...*y_k = y
			if (!publicKey.getElement(1).equals(multiplicationElement)) {
				throw new Exception("The public key is different from the ones the partial keys generate");
			}
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		
		logStringNewLine("OK", false);
		return (true);
	}
	
	// reads the first array of cipher texts
	private boolean readArrayOfInputCiphertexts()
	{
		// create a new ciphertexts array
		this.allCipherTexts = new ProductElement[this.lambda + 1][];
		this.latestCipherTextIndex = 0;
		
		String filename = "";
		// find out which file should be used as the input file
		if (null == ciphertextsFile)
			filename = getFilepathInFolder("CiphertextList00.bt");
		else
			filename = ciphertextsFile;
		// read the array
		boolean result = readArrayOfCiphertexts(filename, (null != ciphertextsFile), "Step 6. ");
		// if succeeded, than save the current array as the input one
		if (result) {
			this.inputCipherTexts = this.currentCipherTexts;
			this.numCipherTexts = this.inputCipherTexts.length;
			if (this.maxCipherTexts == 0)
				this.maxCipherTexts = this.numCipherTexts;
		}
		// return the result of the reading
		return result;
	}
	
	// read an array of cipher texts from the specific file
	private boolean readArrayOfCiphertexts(String fileName, boolean useReadText, String logStringPrefix)
	{
		// try to read the array of input ciphertexts
		logString(logStringPrefix + "Reading the Array of Ciphertexts : ", true);
		
		// read the keys from the CiphertextList<l>.bt file
		try {
			ByteTree cipherTextsTree = null;
			if (!useReadText) {
				cipherTextsTree = readBTFile(fileName);
			} else {
				cipherTextsTree = readBTTextFile(fileName);
			}
			
			// if failed to read the ByteTree
			if (null == cipherTextsTree) {
				throw new Exception("Failed to read the ciphertexts file");
			}
			// save the current into the previous list
			this.prevCipherTexts = this.currentCipherTexts;
			// now, convert the byte tree into a product group element array
			this.currentCipherTexts = ArithmeticConvertor.ToProductElementArray(cipherTextsTree, ciphertextsGroup);
			this.allCipherTexts[this.latestCipherTextIndex] = this.currentCipherTexts;
			this.latestCipherTextIndex++;
			// check that the size of the array does not exceeds the global size of the arrays
			if (((0 != this.maxCipherTexts) && (this.maxCipherTexts < this.currentCipherTexts.length)) || 
					((null != this.inputCipherTexts) && (this.inputCipherTexts.length != this.currentCipherTexts.length))) {
				throw new Exception("Number of ciphertexts is too big");
			}
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		
		logStringNewLine("OK", false);
		return (true);
	}
	
	// run a shuffle of commitments for server no. l
	private boolean shuffleOfCommitments(int l)
	{
		// first, read the permutation, the proof and the reply.
		logString("Verify Proof of Shuffle of Commitments : ", true);
		
		// read the keys from the PermutationCommitment<l>.bt file
		try {
			ByteTree permutationTree = readBTFile(getFilepathInFolder("PermutationCommitment" + String.format("%02d", l)  + ".bt"));
			
			// if failed to read the ByteTree
			if (null == permutationTree) {
				throw new Exception("Failed to read the permutation commitment file");
			}
			// now, convert the byte tree into a  group element array
			this.permutationCommitment = ArithmeticConvertor.ToGroupElementArray(permutationTree, worldGroup);
			// check that the size of the array equals to the global size of the arrays
			if (this.maxCipherTexts != this.permutationCommitment.length) {
				throw new Exception("Number of permutations is wrong");
			}
			// now, read the commitment and the reply as byte array
			byte[] commitment = readBTFileToBytes(getFilepathInFolder("PoSCommitment" + String.format("%02d", l)  + ".bt"));
			byte[] reply = readBTFileToBytes(getFilepathInFolder("PoSReply" + String.format("%02d", l)  + ".bt"));
			
			if ((null == commitment) || (null == reply)) {
				throw new Exception("Failed to read the proof commitment and the proof reply files");
			}
			
			// now, run the Fiat-Shamir algorithm - Algorithm No.16
			info.init(this.prefix, l, this.maxCipherTexts, this.numVectorBits, 
					this.acceptedError, this.numChallengeBits, this.prg, worldGroup, 
					permutationCommitment, commitment, reply, this.hash);
			
			ShuffleOfCommitmentsProofVerifier verifier = new ShuffleOfCommitmentsProofVerifier(info);
			if (false == verifier.validateProof()) {
				throw new Exception("Proof Failed");
			}
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		// all went well
		logStringNewLine("OK", false);
		return (true);
	}
	
	// shrink the permutation using the keep list file or using a pre-defined keep list as described in the verifier's algorithm
	private void shrinkPermutation(int l)
	{
		// shrink the permutation
		logString("Shrink Permutation Commitment : ", true);
		
		// read the keep-list from KeepList<l>.bt file
		try {
			// now, convert the byte tree into an array of booleans
			boolean[] keepList = null;
			try {
				ByteTree keepListTree = readBTFile(getFilepathInFolder("KeepList" + String.format("%02d", l)  + ".bt"));
				
				// if failed to read the ByteTree
				if (null == keepListTree) {
					throw new Exception("Failed to read the keep list file");
				}
				
				keepList = ArithmeticConvertor.ToBooleanArray(keepListTree);
				if (this.maxCipherTexts != keepList.length) {
					throw new Exception("Number of booleans is wrong");
				}
				int count = 0;
				for (int i = 0; i < keepList.length; i++) {
					count += (keepList[i] ? 1 : 0);
				}
				
				if (this.numCipherTexts != count) {
					throw new Exception("Number of true booleans is wrong");
				}
			} catch (Exception e) {
				keepList = new boolean[this.maxCipherTexts];
				for (int i = 0 ; i < this.numCipherTexts; i++) {
					keepList[i] = true;
				}
			}
			
			// now, we should remove all the unneeded values from the permutations array
			GroupElement[] newPermutationsArray = new GroupElement[this.numCipherTexts];
			for (int i = 0, j = 0; i < this.numCipherTexts; i++, j++) {
				// move over all the booleans until true is found
				for (; j < keepList.length; j++) {
					if (true == keepList[j])
					{ break; }
				}
				
				if (j < keepList.length)
				{ newPermutationsArray[i] = this.permutationCommitment[j]; }
			}
			// save the shrunk array
			this.permutationCommitment = newPermutationsArray;
			// all went well
			logStringNewLine("OK", false);
		} catch (Exception e) {
			// this step should never fail
			logStringNewLine("OK", false);
			logError(e.getMessage());
		}
	}
	
	// run a shuffle of commitment consistent proof for server no.l
	private boolean shuffleOfCommitmentConsistent(int l)
	{
		// first, read the permutation, the proof and the reply.
		logString("Verify Commitment-Consistent Proof of Shuffle : ", true);
		
		try {
			// now, read the commitment and the reply as byte array
			byte[] commitment = readBTFileToBytes(getFilepathInFolder("CCPoSCommitment" + String.format("%02d", l)  + ".bt"));
			byte[] reply = readBTFileToBytes(getFilepathInFolder("CCPoSReply" + String.format("%02d", l)  + ".bt"));
			
			if ((null == commitment) || (null == reply)) {
				throw new Exception("Failed to read the proof commitment and the proof reply files");
			}
			
			// now, run the Fiat-Shamir algorithm - Algorithm No.17
			info.init(this.prefix, l, this.numCipherTexts, this.numVectorBits, 
					this.acceptedError, this.numChallengeBits, this.prg, worldGroup, ciphertextsGroup,
					randomizersField, permutationCommitment, publicKey, this.prevCipherTexts,
					this.currentCipherTexts, commitment, reply, this.hash);
			
			ShuffleOfCiphertextsConsistentCommitmentProofVerifier verifier = new ShuffleOfCiphertextsConsistentCommitmentProofVerifier(info);
			if (false == verifier.validateProof()) {
				throw new Exception("Proof Failed");
			}
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		// all went well
		logStringNewLine("OK", false);
		return (true);
	}
	
	// runs a proof of shuffle for each server
	private boolean proofsOfShuffle()
	{
		// try to verify the shuffling and the ciphering
		logStringNewLine("", false); // print empty line
		logStringNewLine("Step 7. Proofs of Shuffle", true);
		logStringNewLine("===============================================", false);
		logStringNewLine("", false); // print empty line
		
		try {
			// iterate over each mix-server
			for (int i = 1 ; i <= this.lambda; i++) {
				logStringNewLine("Server No." + String.format("%02d", i), true);
				logStringNewLine("==================================", false);
				
				// run the shuffle of commitments proof
				boolean shuffleResult = shuffleOfCommitments(i);
				
				if (false == shuffleResult) {
					if ((null == info.independentGenerators()) || (0 == info.independentGenerators().length)) {
						// re-init the info so the generators would get created
						info.init(this.prefix, i, this.numCipherTexts, this.numVectorBits, 
								this.acceptedError, this.numChallengeBits, this.prg, worldGroup, 
								null, null, null, this.hash);
					}
					// set u_l = h
					this.permutationCommitment = info.independentGenerators();
				}
				
				// Shrink permutation commitment
				shrinkPermutation(i);
				
				// read the array of ciphertexts
				if (false == readArrayOfCiphertexts(getFilepathInFolder("CiphertextList" + String.format("%02d", i)  + ".bt"), false, "")) {
					// need to reject
					throw new Exception("Failed to read the ciphertexts");
				}
				// run the shuffle of commitment-consistent proof
				shuffleResult = shuffleOfCommitmentConsistent(i);
				
				if (false == shuffleResult) {
					// check that L_l-1 = L_l
					for (int j = 0; j < this.prevCipherTexts.length; j++) {
						// if the j-th element is different
						if (!prevCipherTexts[j].equals(currentCipherTexts[j])) {
							// we should reject
							throw new Exception("The ciphertexts are different from step l-1 and step l, while they shouldn't");
						}
					}
				}
				logStringNewLine("", false); // print empty line
			}
		} catch (Exception e) {
			logStringNewLine("========================================================", false);
			logStringNewLine("Step 7. Proofs of Shuffle : Failed", true);
			logError(e.getMessage());
			return (false);
		}
		
		logStringNewLine("====================================================", false);
		logStringNewLine("Step 7. Proofs of Shuffle : OK", true);
		return (true);
	}
	
	// reads the secret key for party no.l, if one exists
	private boolean readSecretKey(int l)
	{
		// first, read the permutation, the proof and the reply.
		logString("Check for Secret Key : ", true);
		boolean keyFileFound = true;
		
		try {
			ByteTree publicKeyPower = readBTFile(getFilepathInFolder("SecretKey" + String.format("%02d", l)  + ".bt"));
			
			// if failed to read the ByteTree
			if (null == publicKeyPower) {
				keyFileFound = false;
				throw new Exception("Failed to read the secret key file");
			}
			// now, convert the byte tree into a field element
			FieldElement x = ArithmeticConvertor.ToFieldElement(publicKeyPower, info.worldField());
			
			// check if y_l = g^x
			if (!individualPublicKeys[l].equals(worldGroup.getGroupCreatorAsGroupElement().power(x.getElement()))) {
				throw new Exception("The secret key does not match the partial public key");
			}
			
			// here, we should create f_l
			this.allDecryptionFactors[l - 1] = new GroupElement[allCipherTexts[l - 1].length];
			for (int i = 0; i < this.allDecryptionFactors[l - 1].length; i++) {
				// run element by element and calculate PDec_x_l(L_l-1)
				if (2 > allCipherTexts[l - 1][i].getNumberOfElements()) {
					throw new Exception("The ciphertexts are malformed");
				}
				this.allDecryptionFactors[l - 1][i] = allCipherTexts[l - 1][i].getElement(0).power(x.getElement());
			}
		} catch (Exception e) {
			logStringNewLine("None", false);
			// show the message only when the problem is not about finding the key file
			if (true == keyFileFound) {
				logError(e.getMessage());
			}
			return (false);
		}
		// all went well
		logStringNewLine("OK", false);
		return (true);
	}
	
	// run a decryption proof for party no. l
	private boolean decryptionProof(int l)
	{
		logString("Verify Proofs of Decryption : ", true);
		
		try {
			// read f_l:
			ByteTree decryptionFactorsTree = readBTFile(getFilepathInFolder("DecryptionFactors" + String.format("%02d", l)  + ".bt"));
			// if failed to read the ByteTree
			if (null == decryptionFactorsTree) {
				throw new Exception("Failed to read the decryption factor file");
			}

			// now, we need to read both the partial public key and the decryption factors
			ByteTreeElement rootElem = decryptionFactorsTree.getFirstByteTreeElement();
			if (!(rootElem instanceof Node)) {
				throw new Exception("the decryption factor was malformed");
			}
			Node rootNode = (Node)rootElem;
			// check that the root has 2 children
			if (2 != rootNode.getDataLength()) {
				throw new Exception("the decryption factor doesn't have the supported number of children");
			}
			// read y_l and f_l
			GroupElement partialPublicKey = ArithmeticConvertor.ToGroupElement(new ByteTree(rootNode.getChild(0)), worldGroup);
			GroupElement[] decryptionFactors = ArithmeticConvertor.ToGroupElementArray(new ByteTree(rootNode.getChild(1)), worldGroup);
			
			// check that the partial public key is correct
			if (!partialPublicKey.equals(individualPublicKeys[l - 1])) {
				throw new Exception("The public key is incorrect");
			}
			// check that the array is of the correct size
			if (inputCipherTexts.length != decryptionFactors.length) {
				throw new Exception("Number of decryption factors is incorrect");
			}
			// save the decryption factors
			this.allDecryptionFactors[l - 1] = decryptionFactors;
			
			// read the commitment and the reply as byte array
			byte[] commitment = readBTFileToBytes(getFilepathInFolder("DecrFactCommitment" + String.format("%02d", l)  + ".bt"));
			byte[] reply = readBTFileToBytes(getFilepathInFolder("DecrFactReply" + String.format("%02d", l)  + ".bt"));
			
			if ((null == commitment) || (null == reply)) {
				throw new Exception("Failed to read the proof commitment and the proof reply files");
			}
			
			// now, run the Fiat-Shamir algorithm - Algorithm No.17
			info.init(this.prefix, l, this.numCipherTexts, this.numVectorBits, 
					this.acceptedError, this.numChallengeBits, this.prg, worldGroup, plaintextsGroup, 
					individualPublicKeys[l - 1], allCipherTexts[allCipherTexts.length - 1], decryptionFactors,
					commitment, reply, this.hash);
			// run the proof
			CorrectDecryptionFactorsProofVerifier verifier = new CorrectDecryptionFactorsProofVerifier(info);
			if (false == verifier.validateProof()) {
				throw new Exception("Proof Failed");
			}
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		// all went well
		logStringNewLine("OK", false);
		return (true);
	}
	
	// check for proof of decryption by iterating over all the parties and check that each one passes the decryption proof
	private boolean proofsOfDecryption()
	{
		// try to check that the decryption is ok
		logStringNewLine("", false); // print empty line
		logStringNewLine("Step 8. Proofs of Decryption", true);
		logStringNewLine("==================================================", false);
		logStringNewLine("", false); // print empty line
		
		// create the decryption factors array
		this.allDecryptionFactors = new GroupElement[this.numParties][];
		try {
			// iterate over each mix-server
			for (int i = 1 ; i <= this.numParties; i++) {
				logStringNewLine("Party No." + String.format("%02d", i), true);
				logStringNewLine("=================================", false);
				
				// check if we can read the secret key - if so, the party was identified as a cheater and no need to check the proof
				if (readSecretKey(i)) {
					continue;
				}
				// couldn't read the secret key - we should run the decryption proof
				if (!decryptionProof(i)) {
					// we should reject
					throw new Exception("The decryption factors proof has failed");					
				}
				logStringNewLine("", false); // print empty line
			}
		} catch (Exception e) {
			logStringNewLine("===========================================================", false);
			logStringNewLine("Step 8. Proofs of Decryption : Failed", true);
			logError(e.getMessage());
			return (false);
		}
		
		logStringNewLine("=======================================================", false);
		logStringNewLine("Step 8. Proofs of Decryption : OK", true);
		return (true);
	}
	
	// finally, verify the output of all the parties
	private boolean verifyOutput()
	{
		// try to check that the output is ok
		logString("Step 9. Verify Output : ", true);
		
		try {
			// read the plain texts
			ByteTree plainTextsTree = null;
			
			if (null == plaintextsFile)
				plainTextsTree = readBTFile(getFilepathInFolder("PlaintextElements.bt"));
			else
				plainTextsTree = readBTTextFile(plaintextsFile);
			
			// if failed to read the ByteTree
			if (null == plainTextsTree) {
				throw new Exception("Failed to read the plain texts file");
			}
			// read m - here the plain texts group is the world group
			GroupElement[] plainTexts = ArithmeticConvertor.ToGroupElementArray(plainTextsTree, plaintextsGroup);
			
			// check that the array is of the correct size
			if (inputCipherTexts.length != plainTexts.length) {
				throw new Exception("Number of plain texts is incorrect");
			}
			
			// now, calculate TDec(L_lambda, f_1*...*f_k)
			GroupElement[] f = new GroupElement[this.allDecryptionFactors[0].length];
			// first, initialize all the elements in f to be 1
			for (int j = 0; j < f.length; j++) {
				f[j] = worldGroup.getGroupIdentityElement();
			}
			
			// now, calculate f as f_1*...*f_k
			for (int i = 0 ; i < this.allDecryptionFactors.length; i++) {
				for (int j = 0; j < f.length; j++) {
					f[j] = f[j].multiply(this.allDecryptionFactors[i][j]);
				}
			}
			
			// lastly, calculate L_lambda / f
			for (int i = 0; i < f.length; i++) {
				if (!plainTexts[i].equals(allCipherTexts[allCipherTexts.length - 1][i].getElement(1).divide(f[i]))) {
					throw new Exception("The ciphered texts are corrupted");
				}
			}
		} catch (Exception e) {
			logStringNewLine("Failed", false);
			logError(e.getMessage());
			return (false);
		}
		// all went well
		logStringNewLine("OK", false);
		return (true);
	}
	
	/**
	 * Reads a byte tree file which is in the form of a text file, and returns the byte tree which is contained in it.
	 * 
	 * @param filename the file to read from.
	 * @return Byte Tree which is saved in the file.
	 */
	public static ByteTree readBTTextFile(String filename)
	{
		// read the entire file
		FileReader r = null;
		try {
			// open the file
			r = new FileReader(filename);
			BufferedReader bufRead = new BufferedReader(r);
			// create a string builder to keep all the lines from the file
			StringBuilder sb = new StringBuilder();
			String line = "";
			// iterate over all the lines in the file and add them to the string-builder
			while (null != line) {
				// read line
				line = bufRead.readLine();
				if (null != line) {
					// save the line
					sb.append(line);
				}
			}
			
			bufRead.close();
			// return the created string
			return (new ByteTree(sb.toString()));
		} catch (Exception e) {
			return (null);
		} finally {
			// close the file if it is still open
			if (null != r) {
				try {
					r.close();
				} catch (IOException e) {}
			}
		}
	}
	
	/**
	 * Reads a byte tree file which is in the form of a binary file, and returns the byte tree which is contained in it.
	 * 
	 * @param fileName the file to read from.
	 * @return Byte Tree which is saved in the file.
	 */
	public static ByteTree readBTFile(String fileName) {
		// read the bytes in the file
		byte[] bArr = readBTFileToBytes(fileName);
		
		if (null == bArr) {
			return (null);
		}
		// create a byte tree from the bytes in the file
		return (new ByteTree(bArr));
	}
	
	/**
	 * Reads a byte tree file which is in the form of a binary file, and returns all the bytes that are in it.
	 * 
	 * @param fileName the file to read from.
	 * @return array of bytes which represents the contents of the file.
	 */
	public static byte[] readBTFileToBytes(String fileName) {
		byte[] bArr = null;
		InputStream inStream = null;
		
		try {
			// open the file
			File inputFile = new File(fileName);
			inStream = new FileInputStream(inputFile);
			// get the size of the file
			long fileSize = inputFile.length();
			bArr = new byte[(int) fileSize];
			// read all the contents of the file
			inStream.read(bArr, 0, (int)fileSize);
		} catch (Exception e) {
			return (null);
		} finally {
			// close the stream
			if (null != inStream)
				try {
					inStream.close();
				} catch (IOException e) {}
		}
		
		return (bArr);
	}
	
	// combine the path to the folder with the name of the file
	private String getFilepathInFolder(String filename) {
		return (new File(new File(this.proofDirectory), filename).getPath());
	}
}
